Skip to content

[Anima] Add img2img pipeline blocks#13929

Merged
yiyixuxu merged 13 commits into
huggingface:mainfrom
PreethamNoelP:anima-img2img
Jul 16, 2026
Merged

[Anima] Add img2img pipeline blocks#13929
yiyixuxu merged 13 commits into
huggingface:mainfrom
PreethamNoelP:anima-img2img

Conversation

@PreethamNoelP

@PreethamNoelP PreethamNoelP commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

This PR adds image-to-image (img2img) support to the Anima modular pipeline, as requested in #13903. The implementation follows the z_image modular pattern: img2img is a workflow inside the unified AnimaAutoBlocks, selected automatically when an image (or pre-encoded image_latents) is provided.

Design

New blocks

  • AnimaImg2ImgVaeEncoderStep — preprocesses the input image and encodes it with the VAE, returning image_latents only. Encoding is independent of strength / num_inference_steps / num_images_per_prompt, so latents encoded once can be reused across settings.
  • AnimaImageInputStep — input processing step (analogous to AnimaTextInputStep) that expands image_latents to the final denoising batch (batch_size * num_images_per_prompt) and derives height/width from the latents when not provided.
  • AnimaImg2ImgSetTimestepsStep — computes the full timestep schedule, then slices it by strength via get_timesteps(), which also sets the scheduler's begin index.
  • AnimaImg2ImgPrepareLatentsStep — generates noise and mixes it with the image latents via scheduler.scale_noise() at the first sliced timestep, and creates the Cosmos padding mask.
  • AnimaImg2ImgCoreDenoiseStep — flat SequentialPipelineBlocks: text_conditioning → input → image_input → set_timesteps → prepare_latents → denoise, mirroring ZImageImage2ImageCoreDenoiseStep.
  • AnimaAutoVaeImageEncoderStepAutoPipelineBlocks triggered by image; skipped for text2image.
  • AnimaAutoDenoiseStepAutoPipelineBlocks with block_trigger_inputs = ["image_latents", None], selecting img2img or text2image core denoise.

Unified pipeline

AnimaAutoBlocks = text_encoder → vae_encoder (auto) → denoise (auto) → decode, with a _workflow_map covering both text2image and img2img. No separate img2img pipeline class.

Usage

import torch
from diffusers import AnimaModularPipeline
from diffusers.modular_pipelines.anima import AnimaAutoBlocks

pipe = AnimaModularPipeline(
    blocks=AnimaAutoBlocks(),
    pretrained_model_name_or_path="circlestone-labs/Anima-Base-v1.0-Diffusers",
)
pipe.load_components(torch_dtype=torch.bfloat16)
pipe.to("cuda")

image = pipe(
    prompt="masterpiece, best quality, mountain landscape at golden hour, snow-capped peaks, pine forest, dramatic clouds, cinematic, detailed",
    image=input_image,
    strength=0.75,
    num_inference_steps=28,
    generator=torch.Generator(device="cuda").manual_seed(42),
).images[0]

Test results

pytest tests/modular_pipelines/anima/ -q
28 passed, 10 skipped

Fixes #13903

Before submitting

  • This PR fixes a typo or improves the docs (you can dismiss the other checks if that's the case).
  • Did you read the contributor guideline?
  • Did you read our philosophy doc (important for complex PRs)?
  • Was this discussed/approved via a GitHub issue or the forum? Link: [Anima] Add img2img capability #13903
  • Did you make sure to update the documentation with your changes?
  • Did you write any new necessary tests?

Who can review?

@asomoza @yiyixuxu

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests modular-pipelines utils fixes-issue size/L PR with diff > 200 LOC labels Jun 12, 2026
@asomoza

asomoza commented Jun 15, 2026

Copy link
Copy Markdown
Member

thanks @PreethamNoelP, can we start with updating the PR with a code snippet to test it and the images to see that it works before doing the review.

@PreethamNoelP

Copy link
Copy Markdown
Contributor Author

Hi @asomoza, here is the code snippet and output images.

Usage:

import torch
from diffusers import AnimaModularPipeline
from diffusers.modular_pipelines.anima import AnimaImg2ImgAutoBlocks

pipe = AnimaModularPipeline(
    blocks=AnimaImg2ImgAutoBlocks(),
    pretrained_model_name_or_path="circlestone-labs/Anima-Base-v1.0-Diffusers",
)
pipe.load_components(torch_dtype=torch.bfloat16)
pipe.to("cuda")

image = pipe(
    prompt="masterpiece, best quality, mountain landscape at golden hour, snow-capped peaks, pine forest, dramatic clouds, cinematic, detailed",
    image=input_image,
    strength=0.75,
    num_inference_steps=28,
    generator=torch.Generator(device="cuda").manual_seed(42),
).images[0]

Outputs:
side_by_side_comparison

Comment on lines +432 to +433
# Copied from diffusers.modular_pipelines.anima.before_denoise.AnimaSetTimestepsStep
class AnimaImg2ImgSetTimestepsStep(ModularPipelineBlocks):

@asomoza asomoza Jun 17, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you used the copied from here but you changed the function no? You can catch these errors and the code quality by following the contributing doc

@PreethamNoelP PreethamNoelP Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the # Copied from tag above AnimaImg2ImgSetTimestepsStep.
Since the class intentionally skips the set_begin_index(0) call (that's handled later by the VAE encoder step), it was never truly identical to the source, so the tag shouldn't have been there.
Please let me know if any further changes are required.



# auto_docstring
class AnimaImg2ImgAutoBlocks(SequentialPipelineBlocks):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do not need a AnimaImg2ImgAutoBlocks. We add a workflow into the AnimaAutoBlocks

see example https://github.com/huggingface/diffusers/blob/main/src/diffusers/modular_pipelines/z_image/modular_blocks_z_image.py#L325

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed AnimaImg2ImgAutoBlocks entirely and folded img2img into AnimaAutoBlocks following the z_image pattern.
I added AnimaAutoDenoiseStep (an AutoPipelineBlocks that picks between img2img and txt2img based on whether image is provided) and AnimaImg2ImgDenoiseStep (a SequentialPipelineBlocks that wraps the img2img-specific set_timesteps + denoise steps).
AnimaAutoBlocks now has a _workflow_map covering both workflows, so users only need one pipeline - passing image= automatically triggers img2img.
Please let me know if any further changes are required.

…nto AnimaAutoBlocks

- Remove incorrect `# Copied from` comment above AnimaImg2ImgSetTimestepsStep
- Delete AnimaImg2ImgAutoBlocks; introduce AnimaAutoDenoiseStep (AutoPipelineBlocks)
  and AnimaImg2ImgDenoiseStep (SequentialPipelineBlocks) so img2img lives as a
  workflow inside AnimaAutoBlocks, following the z_image pattern
- Update __init__.py, dummy_objects, and docs to remove AnimaImg2ImgAutoBlocks
- Update img2img test to use AnimaAutoBlocks with updated workflow block paths
@github-actions github-actions Bot removed documentation Improvements or additions to documentation utils labels Jun 19, 2026
@asomoza

asomoza commented Jun 26, 2026

Copy link
Copy Markdown
Member

@PreethamNoelP thanks, sorry I missed the update, I checked it and it looks good to me, I tested it and the outputs seems coherent too.

@yiyixuxu can you review it one last time to see if I missed something?

@asomoza

asomoza commented Jun 26, 2026

Copy link
Copy Markdown
Member

@bot /style

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Style bot fixed some files and pushed the changes.

@HuggingFaceDocBuilderDev

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

@ghunkins

ghunkins commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Any updates on this by chance? Would love to see this in main! cc @yiyixuxu

)

@property
def inputs(self) -> list[InputParam]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the encoder step should just take an image (height/width too) and return image_latents,

i.e. if you have already encoded the image once, you should not need to encode again just to use a different strength/num_images_per_prompt or num_inference_steps etc

see https://github.com/huggingface/diffusers/blob/main/src/diffusers/modular_pipelines/z_image/encoders.py#L272

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @yiyixuxu, thank you for the suggestion. AnimaImg2ImgVaeEncoderStep has been refactored to only handle image encoding, returning image_latents. The timestep slicing and noise mixing logic has been extracted into a new AnimaImg2ImgPrepareLatentsStep, following the same separation as ZImageVaeImageEncoderStep and ZImagePrepareLatentswithImageStep.


block_classes = [AnimaImg2ImgDenoiseStep, AnimaCoreDenoiseStep]
block_names = ["img2img", "text2image"]
block_trigger_inputs = ["image", None]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
block_trigger_inputs = ["image", None]
block_trigger_inputs = ["image_latents", None]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @yiyixuxu, thank you for the suggestion. AnimaAutoDenoiseStep has been updated to use block_trigger_inputs = ["image_latents", None]. Additionally, AnimaAutoVaeImageEncoderStep (triggered by "image") has been added as a sequential step in AnimaAutoBlocks, ensuring image_latents is placed in state before the denoise step selects the appropriate block - consistent with the ZImageAutoBlocks structure.

…ImageEncoderStep

- AnimaImg2ImgVaeEncoderStep now only encodes image → image_latents
- AnimaImg2ImgPrepareLatentsStep handles timestep slicing + noise mixing
- AnimaAutoVaeImageEncoderStep (AutoPipelineBlocks, trigger: image) added to AnimaAutoBlocks
- AnimaAutoDenoiseStep trigger changed from image to image_latents
- Follows z_image pattern (ZImageVaeImageEncoderStep + ZImagePrepareLatentswithImageStep)
Comment on lines +594 to +596
block_state.timesteps, block_state.num_inference_steps = get_timesteps(
components.scheduler, block_state.num_inference_steps, block_state.strength
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we move this into AnimaImg2ImgSetTimestepsStep? makes more sense there

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @yiyixuxu, thank you for the suggestion.
The get_timesteps() call has been moved into AnimaImg2ImgSetTimestepsStep, which now computes the full schedule and slices it by strength in a single step. AnimaImg2ImgPrepareLatentsStep no longer touches the timestep schedule.

components.scheduler, block_state.num_inference_steps, block_state.strength
)

total_batch = block_state.batch_size * block_state.num_images_per_prompt

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in modular, we should expand the image_latent in the input step , you can add an image_input step similar to https://github.com/huggingface/diffusers/blob/main/src/diffusers/modular_pipelines/anima/before_denoise.py#L196

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @yiyixuxu, thank you for the suggestion.
A new AnimaImageInputStep has been added following the same pattern as AnimaTextInputStep - it expands image_latents to batch_size * num_images_per_prompt via repeat_tensor_to_batch_size and derives height/width from the latents when not provided. AnimaImg2ImgPrepareLatentsStep now only handles noise generation and scale_noise().

return [OutputParam.template("latents")]


# auto_docstring

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this? can you just add the set_timesteps as part of core_denoise?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @yiyixuxu, you're right - the wrapper was left over from an earlier structure and is no longer needed. AnimaImg2ImgDenoiseStep has been removed, and AnimaImg2ImgSetTimestepsStep is now part of AnimaImg2ImgCoreDenoiseStep directly, making it a flat sequence (text_conditioning → input → image_input → set_timesteps → prepare_latents → denoise) consistent with ZImageImage2ImageCoreDenoiseStep.

@PreethamNoelP PreethamNoelP requested a review from yiyixuxu July 16, 2026 03:03


# auto_docstring
class AnimaAutoDenoiseStep(AutoPipelineBlocks):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class AnimaAutoDenoiseStep(AutoPipelineBlocks):
class AnimaAutoCoreDenoiseStep(AutoPipelineBlocks):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @yiyixuxu, thank you for the suggestion.
AnimaAutoDenoiseStep has been renamed to AnimaAutoCoreDenoiseStep, consistent with the naming used across the other modular pipelines.

enable_full_determinism()


ANIMA_IMG2IMG_WORKFLOWS = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @yiyixuxu, done - the img2img test class has been moved into test_modular_pipeline_anima.py and the separate test file has been removed.

@PreethamNoelP PreethamNoelP requested a review from yiyixuxu July 16, 2026 11:09

@yiyixuxu yiyixuxu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks

@yiyixuxu yiyixuxu merged commit e11810a into huggingface:main Jul 16, 2026
18 of 19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Anima] Add img2img capability

5 participants